using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using ChessKit.Common;
using ChessKit.Ui.ChessPanel;
using Vector = System.Windows.Vector;

namespace ChessKit.Ui
{
  public class SlaveBoardPanel : Panel
  {
    private readonly SlaveLayout _slaveLayout = new SlaveLayout();

    public SlaveBoardPanel()
    {
      Loaded += (s, e) => BoardPanel.AddLocationChangedHandler(this, Handler);
      Unloaded += (s, e) => BoardPanel.RemoveLocationChangedHandler(this, Handler);
      Layout = _slaveLayout;
    }

    private void Handler(object sender, RoutedEventArgs routedEventArgs)
    {
      AnimateLocationChange((DependencyObject)routedEventArgs.OriginalSource);
    }

    #region ' RelocateAnimation : BoardPanelAmimationOptions (Dependency Property) '

    public BoardPanelAmimationOptions RelocateAnimation
    {
      get { return (BoardPanelAmimationOptions)GetValue(RelocateAnimationProperty); }
      set { SetValue(RelocateAnimationProperty, value); }
    }

    public static readonly DependencyProperty RelocateAnimationProperty =
      DependencyProperty.Register("RelocateAnimation",
                                  typeof(BoardPanelAmimationOptions),
                                  typeof(SlaveBoardPanel), new PropertyMetadata(null));

    #endregion

    #region ' AnimationOffsetX : double (Attached Property) '

    // TODO: Make AnimationOffsetX private 

    public static readonly DependencyProperty AnimationOffsetXProperty =
      DependencyProperty.RegisterAttached("AnimationOffsetX",
                                          typeof(double), typeof(SlaveBoardPanel),
                                          new FrameworkPropertyMetadata(default(double),
                                                                        FrameworkPropertyMetadataOptions.AffectsParentMeasure));

    public static double GetAnimationOffsetX(DependencyObject obj)
    {
      return (double)obj.GetValue(AnimationOffsetXProperty);
    }

    public static void SetAnimationOffsetX(DependencyObject obj, double value)
    {
      obj.SetValue(AnimationOffsetXProperty, value);
    }

    #endregion

    #region ' AnimationOffsetY : double (Attached Property) '

    // TODO: Make AnimationOffsetY private 

    public static readonly DependencyProperty AnimationOffsetYProperty =
      DependencyProperty.RegisterAttached("AnimationOffsetY",
                                          typeof(double), typeof(SlaveBoardPanel),
                                          new FrameworkPropertyMetadata(default(double),
                                                                        FrameworkPropertyMetadataOptions.AffectsParentMeasure));

    public static double GetAnimationOffsetY(DependencyObject obj)
    {
      return (double)obj.GetValue(AnimationOffsetYProperty);
    }

    public static void SetAnimationOffsetY(DependencyObject obj, double value)
    {
      obj.SetValue(AnimationOffsetYProperty, value);
    }

    #endregion

    #region ' Animation '

    protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
    {
      if (visualRemoved != null) _elementsToAnimate.Remove(visualRemoved);
      base.OnVisualChildrenChanged(visualAdded, visualRemoved);
    }

    private readonly Dictionary<DependencyObject, Point>
      _locationsCahce = new Dictionary<DependencyObject, Point>();

    private void ArrangeAnimated(UIElement element, Point location, Size size)
    {
      var isToBeAnimated = _elementsToAnimate.Contains(element);

      var oldLocation = default(Point);
      if (isToBeAnimated)
        if (!_locationsCahce.TryGetValue(element, out oldLocation))
          isToBeAnimated = false;

      if (isToBeAnimated)
      {
        var offset = oldLocation - location;
        SetupAnimation(element, offset);
        _elementsToAnimate.Remove(element);
        location = oldLocation;
      }
      else
      {
        location.Offset(
          GetAnimationOffsetX(element),
          GetAnimationOffsetY(element));
      }

      _locationsCahce[element] = location;
      element.Arrange(new Rect(location, size));
    }
    private void ArrangeAnimated(UIElement element, Rect rect)
    {
      ArrangeAnimated(element, rect.Location, rect.Size);
    }

    private readonly HashSet<DependencyObject> _elementsToAnimate
      = new HashSet<DependencyObject>();

    private void AnimateLocationChange(DependencyObject element)
    {
      if (RelocateAnimation != null)
      {
        _elementsToAnimate.Add(element);
      }
    }

    private void SetupAnimation(UIElement element, Vector offset)
    {
      RemoveAnimationInProgress(element);

      var storyboard = CreateTransitionStoryboard(element, offset);
      _element2StoryBoard.Add(element, storyboard);
      storyboard.Completed +=
        delegate
          {
            Storyboard value;
            if (!_element2StoryBoard.TryGetValue(element, out value)) return;
            if (value != storyboard) return;
            _element2StoryBoard.Remove(element);
            storyboard.Stop();
          };
      storyboard.Begin();
    }

    private void RemoveAnimationInProgress(DependencyObject element)
    {
      Storyboard storyboard;
      if (!_element2StoryBoard.TryGetValue(element, out storyboard)) return;
      storyboard.Stop();
      _element2StoryBoard.Remove(element);
    }

    private Storyboard CreateTransitionStoryboard(UIElement child, Vector offset)
    {
      var options = RelocateAnimation;

      var createAnimation = new Func<DependencyProperty, double, IEasingFunction, DoubleAnimation>(
        (prop, from, ease) =>
          {
            var animation = new DoubleAnimation
                              {
                                Duration = options.Duration,
                                From = from,
                                To = 0.0,
                                EasingFunction = ease
                              };
            Storyboard.SetTarget(animation, child);
            Storyboard.SetTargetProperty(animation, new PropertyPath(prop));
            return animation;

          });


      var res = new Storyboard
                  {
                    Duration = options.Duration,
                    Children =
                      {
                        createAnimation(AnimationOffsetXProperty, offset.X, options.EaseX),
                        createAnimation(AnimationOffsetYProperty, offset.Y, options.EaseY),
                      }
                  };
      res.Completed += (s, e) => child.RaiseEvent(
        new RoutedEventArgs(AnimationCompletedEvent));
      return res;
    }

    private readonly Dictionary<object, Storyboard>
      _element2StoryBoard = new Dictionary<object, Storyboard>();

    public static readonly RoutedEvent AnimationCompletedEvent =
      EventManager.RegisterRoutedEvent("AnimationCompleted",
      RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SlaveBoardPanel));

    #endregion

    #region ' Measure & Arrange '

    internal ISlaveLayout Layout { get; set; }

    protected override Size MeasureOverride(Size availableSize)
    {
      Layout.SlaveMeasure(availableSize);

      foreach (var element in InternalChildren.OfType<UIElement>())
      {
        var location = BoardPanel.GetLocation(element);
        if (location != BoardLocation.Unknown)
        {
          element.Measure(Layout[location].Size);
        }
        else
        {
          var locations = BoardPanel.GetLocations(element);
          if (locations == null) continue;
          var rects = locations.Select(l => Layout[l]).ToList();
          var envelope = rects.Aggregate(Rect.Union);
          element.Measure(envelope.Size);
        }
      }
      
      return Layout.DesiredSize;
    }

    protected override Size ArrangeOverride(Size arrangeSize)
    {
      Layout.SlaveArrange(arrangeSize);

      foreach (var element in InternalChildren.OfType<UIElement>())
      {
        var location = BoardPanel.GetLocation(element);
        if (location != BoardLocation.Unknown)
        {
          ArrangeAnimated(element, Layout[location]);
        }
        else
        {
          var locations = BoardPanel.GetLocations(element);
          if (locations == null) continue;
          var rects = locations.Select(l => Layout[l]).ToList();
          var envelope = rects.Aggregate(Rect.Union);
          element.Arrange(envelope);
          var boarded = element as IBoardedElement;
          if (boarded == null) continue;
          boarded.SetCells(rects.Select(r => RelativeTo(r, envelope)));
        }
      }
      Layout = _slaveLayout;
      return arrangeSize;
    }
    
    private static Rect RelativeTo(Rect inner, Rect outer)
    {
      return new Rect(inner.TopLeft - (Vector)outer.TopLeft, inner.Size);
    }

    #endregion
  }
}